package com.dmj.sqldsl.platform;

import com.dmj.sqldsl.utils.CollectionUtils;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
import org.testcontainers.containers.MSSQLServerContainer;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.OracleContainer;
import org.testcontainers.containers.PostgreSQLContainer;

public class DatabaseContextProvider implements TestTemplateInvocationContextProvider {

  private static final boolean runRealDatabase = Optional.ofNullable(
          System.getProperty(SystemProperties.realDatabase))
      .map(Boolean::parseBoolean).orElse(false);
  private static final Map<H2Mode, DatabaseContainer> containerMap = new HashMap<>();

  static {
    try {
      if (!runRealDatabase) {
        containerMap.put(H2Mode.h2, DatabaseContainer.h2(H2Mode.h2));
        containerMap.put(H2Mode.mysql, DatabaseContainer.h2(H2Mode.mysql));
        containerMap.put(H2Mode.postgresql, DatabaseContainer.h2(H2Mode.postgresql));
        containerMap.put(H2Mode.oracle, DatabaseContainer.h2(H2Mode.oracle));
        containerMap.put(H2Mode.sqlserver, DatabaseContainer.h2(H2Mode.sqlserver));
      } else {
        containerMap.put(H2Mode.real_mysql,
            DatabaseContainer.of(new MySQLContainer<>("mysql:5.7.34"), H2Mode.real_mysql));
        containerMap.put(H2Mode.real_postgresql,
            DatabaseContainer.of(new PostgreSQLContainer<>("postgres:9.6.12"),
                H2Mode.real_postgresql));
        containerMap.put(H2Mode.real_oracle,
            DatabaseContainer.of(new OracleContainer("gvenzl/oracle-xe:21-slim-faststart")
                .withDatabaseName("testDB")
                .withUsername("testUser")
                .withPassword("testPassword"), H2Mode.real_oracle)
        );
        containerMap.put(H2Mode.real_sqlserver,
            DatabaseContainer.of(
                new MSSQLServerContainer<>("mcr.microsoft.com/mssql/server:2017-CU12")
                    .acceptLicense(), H2Mode.real_sqlserver)
        );
      }
      for (DatabaseContainer container : containerMap.values()) {
        Class.forName(container.getDriverClassName());
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  private static final String METHOD_CONTEXT_KEY = "context";

  @Override
  public boolean supportsTestTemplate(ExtensionContext context) {
    Method method = context.getRequiredTestMethod();
    Database annotation = method.getAnnotation(Database.class);
    if (annotation == null) {
      Class<?> testClass = context.getRequiredTestClass();
      annotation = testClass.getAnnotation(Database.class);
    }
    List<H2Mode> values = CollectionUtils.asModifiableList(annotation.value());
    List<H2Mode> excludedValues = CollectionUtils.asModifiableList(annotation.excluded());
    values.removeAll(excludedValues);
    getStore(context).put(METHOD_CONTEXT_KEY, values.toArray(new H2Mode[0]));
    return true;
  }

  private ExtensionContext.Store getStore(ExtensionContext context) {
    return context.getStore(
        Namespace.create(DatabaseContextProvider.class, context.getRequiredTestMethod()));
  }

  @Override
  public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
      ExtensionContext context) {
    H2Mode[] modes = getStore(context).get(METHOD_CONTEXT_KEY, H2Mode[].class);
    return Arrays.stream(modes).map(containerMap::get)
        .filter(Objects::nonNull)
        .peek(DatabaseContainer::start)
        .map(DatabaseManager::new)
        .map(this::sqlDialectContext);
  }

  private TestTemplateInvocationContext sqlDialectContext(DatabaseManager databaseManager) {
    return new TestTemplateInvocationContext() {
      @Override
      public String getDisplayName(int invocationIndex) {
        return databaseManager.getDisplayName();
      }

      @Override
      public List<Extension> getAdditionalExtensions() {
        return Collections.singletonList(
            new SqlDialectParameterResolver(databaseManager)
        );
      }
    };
  }
}
